package VideoRent.Caja;

import java.io.*;
import java.util.*;

import VideoRent.*;
import VideoRent.Cliente.*;
import VideoRent.Articulo.*;
import VideoRent.Enum.*;
import VideoRent.Alquiler.*;

public class Caja{

    //Elemento que representa el contexto en el cual se encuentra la caja que se
    //desea representar
    private ElementosTienda contexto;
    //Buffer en el cual se deben escribir las acciones de los empleados
    private BufferedWriter escritorE;
    //Buffer en el cual se deben generar las facturas
    private BufferedWriter escritorF;
    //Cuentas abiertas actualmente en la caja
    private ArrayList<Cuenta> cuentas;
    //Dia en el cual se encuentra la caja
    private Calendar diaActual;
    //Variable que permite decidir si la fecha se debe incrementar con la llamada
    //a cambioDia()
    private boolean incrementar;

    /**
     * Constructor de la clase
     * @param contexto Elemento que representa el contexto en el cual se encuentra la caja que se desea representar
     * @param escritorE Buffer en el cual se deben escribir las acciones de los empleados
     * @param escritorF Buffer en el cual se deben generar las facturas
     */
    public Caja(ElementosTienda contexto, BufferedWriter escritorE, BufferedWriter escritorF, Calendar diaActual){
        this.contexto = contexto;
        this.escritorE = escritorE;
        this.escritorF = escritorF;
        this.cuentas = new ArrayList<Cuenta>();
        this.diaActual = diaActual;
        this.incrementar = false;
    }

    /**
     * Método empleado para asociar un nuevo miembro a la tienda descrita en 'contexto'
     * @param cedula Numero de cédula del cliente a asociar
     * @param nombre Nombre del cliente a asociar
     * @param apellido Apellido del cliente a asociar
     * @param telefono Telefono del cliente a asociar
     * @param direccion Direccion de cobro de la tarjeta de crédito
     * @param numeroTarjeta Número de la tarjeta de crédito
     * @param banco Banco al que pertenece la tarjeta de crédito
     * @param cvv Código de seguridad de la tarjeta de crédito
     * @param vencimiento Fecha de vencimiento de la tarjeta de crédito
     * @param membresía Tipo de membresía con la que se desea asociar el cliente
     */
    public void asociar(int cedula, String nombre, String apellido, int telefono, String direccion, String numeroTarjeta,
                         String banco, int cvv, Calendar vencimiento, Membresia membresia)
        throws VideoRentAccionException
    {
        //Declaración de algunas variables necesarias
        String codigo = "", error, salida;
        Calendar limite;
        EstadoCliente estado;

        //Verificar que no exista un cliente con la misma cédula en el sistema
        ClienteAsociado c = contexto.buscarClienteAsociado(cedula);
        if (null != c){
            try{
                escritorE.write("i & " + c.getCodigo() + " & 5");
                escritorE.newLine();
            }catch (IOException e){
                throw new VideoRentAccionException("No se pudo escribir la falla: " + e.getMessage());
            }
            return;
        }

        //Construir el código del nuevo cliente a agregar
        codigo += (membresia == Membresia.PREMIUM ? "P" : "B");
        int numCodigo = ClienteAsociado.getSiguienteCodigo();
        codigo += numCodigo;
        ClienteAsociado.setSiguienteCodigo(numCodigo + 1);

        //Construir el estado en el que será agregado el cliente
        limite = (Calendar)this.diaActual.clone();
        limite.add(Calendar.DAY_OF_MONTH, -35);
        estado = (limite.before(vencimiento) ? EstadoCliente.SUSPENDIDO : EstadoCliente.ACTIVO);

        //Crear y agregar al cliente al sistema
        ClienteAsociado cliente = new ClienteAsociado(cedula, estado, nombre, apellido, telefono, codigo, direccion,
                                                      numeroTarjeta, banco, cvv, vencimiento);
        contexto.getClientesAsociados().add(cliente);
        try{
            escritorE.write("r & " + codigo);
            escritorE.newLine();
        }catch (IOException e){
            throw new VideoRentAccionException("No se pudo escribir el mensaje para registrar: " + e.getMessage());
        }    
    }

    /**
     * Método utilizado para actualizar los datos de la tarjeta de crédito de
     * un cliente de la videotienda representada en 'contexto'
     *
     * @param codigoCliente Código del cliente que desea realizar la operación
     * @param numeroTarjeta Número de la nueva tarjeta
     * @param banco Nombre del nuevo banco
     * @param codigoSeguridad Código de seguridad de la nueva tarjeta
     * @param vencimiento Fecha de vencimiento de la nueva tarjeta
     */
    public void actualizarTarjeta(String codigoCliente, String numeroTarjeta, String banco,
                         int codigoSeguridad, Calendar vencimiento) throws VideoRentAccionException{

        //Declaración de variables
        ClienteAsociado c;
        String mensaje;

        //Se obtiene del contexto el cliente y se valida que en efecto exista
        c = contexto.buscarClienteAsociado(codigoCliente);
        if(c == null){
            try{
                escritorE.write("i & " + codigoCliente + " 0");
                escritorE.newLine();
            }catch(IOException e){
                throw new VideoRentAccionException("No se pudo escribir la falla: " + e.getMessage());
            }
            return;
        }

        //Dado que se obtuvo el cliente, se procede a modificar los datos de su tarjeta
        c.setNumeroTarjeta(numeroTarjeta);
        c.setBanco(banco);
        c.setCodigoSeguridad(codigoSeguridad);
        c.setFechaVencimiento(vencimiento);

        //Se imprime el mensaje de actualización
        try{
            escritorE.write("t & " + c.getCodigo());
            escritorE.newLine();
        }catch(IOException e){
            throw new VideoRentAccionException("No se pudo escribir la modificacion de tarjeta: " + e.getMessage());
        }

        //Se actualiza el estado del cliente
        c.actualizarEstado(escritorE, this.diaActual);
    }

    /**
     * Método para agregar una compra a un cliente asociado
     *
     * @param codigoC Código del cliente que realizará la compra
     * @param codigoA Código del artículo a comprar
     */
    public void compra(String codigoC, String codigoA)
                    throws VideoRentAccionException{
        
        //Declaración de variables
        Cuenta cuenta;

        if(!existeCodigo(codigoC))
            return;

        //Obtener la cuenta del cliente
        cuenta = getCuenta(codigoC);

        //Agregar el artículo que se desea comprar a la cuenta
        cuenta.agregarCompra(codigoA, this.escritorE);
    }

    /**
     * Método para agregar una compra de un cliente no asociado
     * @param cedula Cédula del cliente que realizará la compra
     * @param nombre Nombre del cliente
     * @param telefono Número de teléfono del cliente
     * @param codigoA código del artículo a comprar
     */
    public void compra(int cedula, String nombre, int telefono, String codigoA)
                    throws VideoRentAccionException{
        
        //Declaración de variables
        Cuenta cuenta;

        //Obtener la cuenta del cliente
        cuenta = getCuenta(cedula);

        //Agregar el artículo que se desea comprar a la cuenta
        cuenta.agregarCompra(codigoA, this.escritorE);
    }

    /**
     * Método para agregar un alquiler a la cuenta de un cliente
     *
     * @param codigoC Código del cliente que realizará el alquiler
     * @param codigoA Código del artículo a alquilar
     */
    public void alquiler(String codigoC, String codigoA)
                    throws VideoRentAccionException{

        //Declaración de variables
        Cuenta cuenta;

        if(!existeCodigo(codigoC))
            return;

        //Obtener la cuenta del cliente
        cuenta = getCuenta(codigoC);
        
        //Agregar el artículo que se desea alquilar a la cuenta
        cuenta.agregarAlquiler(codigoA, this.escritorE, this.diaActual);
    }

    /**
     * Fachada de devolución para cuando se pide búsqueda
     *
     * @param codigoC Código del cliente que realizará la devolución
     * @param codigoA Código del artículo a devolver
     * @param diaActual Dia en el cual se encuentra la tienda
     */
    public void devolverBuscar(String codigoC, String codigoA)
                    throws VideoRentAccionException{
        this.devolver(codigoC, codigoA, true, this.diaActual);
    }

    /**
     * Fachada de devolución para cuando se devuelve por caja
     *
     * @param codigoC Código del cliente que realizará la devolución
     * @param codigoA Código del artículo a devolver
     * @param diaActual Dia en el cual se encuentra la tienda
     */
    public void devolverCaja(String codigoC, String codigoA)
                    throws VideoRentAccionException{
        this.devolver(codigoC, codigoA, false, this.diaActual);
    }

    /**
     * Método para agregar una devolución a la cuenta del cliente
     *
     * @param codigoC Código del cliente que realizará la devolución
     * @param codigoA Código del artículo a devolver
     * @param buscar Indica se se debe ir a buscar el artículo o no
     * @param diaActual Dia en el cual se encuentra la tienda
     */
    private void devolver(String codigoC, String codigoA, boolean buscar, Calendar diaActual)
                    throws VideoRentAccionException{

        //Declaración de variables
        Cuenta cuenta;

        if(!existeCodigo(codigoC))
            return;
        //Obtener la cuenta del cliente
        cuenta = getCuenta(codigoC);

        //Realizar la operación de devolver el artículo
        cuenta.devolverArticulo(codigoA, this.escritorE, buscar, diaActual);
    }

    /**
     * Método para realizar el pago de un cliente asociado
     *
     * @param codigoC Codigo del cliente
     * @param monto Monto entregado por el cliente
     */
    public void pagar(String codigoC, int monto)
                    throws VideoRentAccionException{
        
        //Declaración de variables
        Cuenta cuenta;

        //Se valida que el código sea de un cliente asociado válido
        if(!existeCodigo(codigoC))
            return;

        //Obtener la cuenta del cliente
        cuenta = getCuenta(codigoC);

        //Realizar el pago
        cuenta.pagar(monto, this.escritorE, this.escritorF, diaActual);

        //Cerrar la cuenta
        this.cuentas.remove(cuenta);
    }

    /**
     * Método para realizar el pago de un cliente no asociado
     *
     * @param codigoC Codigo del cliente
     * @param monto Monto entregado por el cliente
     */
    public void pagar(int cedula, int monto)
                    throws VideoRentAccionException{
        
        //Declaración de variables
        Cuenta cuenta;

        //Obtener la cuenta del cliente
        cuenta = getCuenta(cedula);

        //Realizar el pago
        cuenta.pagar(monto, this.escritorE, this.escritorF, diaActual);

        //Cerrar la cuenta
        this.cuentas.remove(cuenta);
    }


    /**
     * Método para ejecutar las acciones necesarias al haber un cambio de día
     */
    public void cambioDia()
        throws VideoRentAccionException
    {

        //Declaración de variables
        Calendar limite;
        ArrayList<Alquiler> perdidos;
        int monto;

        try{
            this.escritorE.write("#");
            this.escritorE.newLine();
        }catch(IOException e){
            throw new VideoRentAccionException("No se pudo escribir el separador de día: " + e.getMessage());
        }

        //Se restauran todos los valores de cuentas no pagadas del dia anterior
        //y se limpia el arreglo de cuentas
        for(Cuenta c : this.cuentas){
            for(Articulo a : c.getComprados())
                a.setCantidad(a.getCantidad() + 1);
            for(Articulo a : c.getAlquilados())
                a.setCantidad(a.getCantidad() + 1);
        }

        this.cuentas = new ArrayList<Cuenta>();
        //Se suma en 1 el dia actual de la caja
        if(!incrementar)
            incrementar = true;
        else
            this.diaActual.add(Calendar.DAY_OF_MONTH, 1);

        //Se verifica, para cada cliente asociado, los artículos que deben
        //considerarse perdidos y que deben ser cargados, si se debe cambiar
        //su estado, y si se le debe llamar
        for(ClienteAsociado c : this.contexto.getClientesAsociados()){
            
            //Se itera sobre los artículos que posee en alquiler
            monto = 0;
            perdidos = new ArrayList<Alquiler>();
            for(Alquiler a : c.getArticulosAlquilados()){

                //Si llego la fecha de devolución del artículo, llamar al cliente
                if((diaActual.get(Calendar.YEAR) == a.getFechaMaximaRetorno().get(Calendar.YEAR)) &&
                    (diaActual.get(Calendar.DAY_OF_MONTH) == a.getFechaMaximaRetorno().get(Calendar.DAY_OF_MONTH)) &&
                    (diaActual.get(Calendar.MONTH) == a.getFechaMaximaRetorno().get(Calendar.MONTH))){
                    try{
                        escritorE.write("l & " + c.getCodigo() + " & " + a.getArticulo().getCodigo() + "\n");
                    }catch(IOException e){
                        throw new VideoRentAccionException("No se pudo escribir la llamada: " + e.getMessage());                        
                    }                    
                }

                //Si el artículo se perdió eliminarlo de los alquileres y cobrarlo
                limite = (Calendar)a.getFechaAlquilada().clone();
                limite.add(Calendar.DAY_OF_MONTH, 30);

                if(limite.before(diaActual)){
                    try{
                        c.getArticulosAlquilados().remove(a);
                        escritorE.write("p & " + c.getCodigo() + " & " + a.getArticulo().getCodigo() + "\n");
                        perdidos.add(a);
                        monto += a.getArticulo().getPrecioVenta();
                    }catch(IOException e){
                        throw new VideoRentAccionException("No se pudo escribir el cobro por pérdida: " + e.getMessage());                        
                    }
                }
            }

            //Generar la factura de artículos perdidos, si hubo
            if(!perdidos.isEmpty()){
                try{
                escritorF.write("---------------------------------------------------------- " + diaActual.get(Calendar.DAY_OF_MONTH) + 
                                "/" + diaActual.get(Calendar.MONTH) + "/" + diaActual.get(Calendar.YEAR));
                    escritorF.newLine();
                    escritorF.write("DATOS DEL CLIENTE");
                    escritorF.newLine();
                    escritorF.write("TIPO DE CLIENTE: " + c.getTipoMembresia());
                    escritorF.newLine();
                    escritorF.write("NOMBRE: " + c.getNombre());
                    escritorF.newLine();
                    escritorF.write("APELLIDO: " + c.getApellido());
                    escritorF.newLine();
                    escritorF.write("CI: " + c.getCedula());
                    escritorF.newLine();
                    escritorF.write("CODIGO: " + c.getCodigo());
                    escritorF.newLine();
                    escritorF.write("-------------- ARTICULOS PERDIDOS -------------");
                    for(Alquiler a : perdidos){
                        escritorF.write(a.getArticulo().getCodigo() + "   " + a.getArticulo().getNombre() + "   " + a.getArticulo().getPrecioVenta());
                        escritorF.newLine();
                    }
                    escritorF.write("--------------------------------- MONTO CANCELADO: " + monto + "\n");
                    
                    escritorE.write("f & " + c.getCodigo() + " & " + monto);
                }catch(IOException e){
                    throw new VideoRentAccionException("No se pudo escribir factura de inicio de dia: " + e.getMessage());
                }
            }

            //Se actualiza su estado
            c.actualizarEstado(this.escritorE, diaActual);
        }
    }

    /**
     * Método privado que permite ubicar cuentas existentes en la caja
     * @param codigo Código del cliente al que le pertenece la cuenta
     * @return Cuenta asociada al código, o null si no existe
     */
    private Cuenta getCuenta(String codigoC){
        Cuenta cuenta;
        for(Cuenta c : this.cuentas){
            if (c.getCliente().getCodigo().equals(codigoC))
                return c;
        }
        cuenta = new Cuenta(codigoC, this.contexto);
        this.cuentas.add(cuenta);
        return cuenta;
    }

    /**
     * Método privado que permite ubicar cuentas existentes en la caja
     * @param cedula cedula del cliente al que le pertenece la cuenta
     * @return Cuenta asociada a la cédula, o null si no existe
     */
    private Cuenta getCuenta(int cedula){
        Cuenta cuenta;
        for(Cuenta c : this.cuentas){
            if (c.getCedula() == cedula)
                return c;
        }
        cuenta = new Cuenta(cedula, this.contexto);
        this.cuentas.add(cuenta);
        return cuenta;
    }

    /**
     * Método privado que valida que un código sea de un cliente existente
     *
     * @param codigoC Código que se desea validar si existe
     * @return Booleano que indica si el código existe
     */
    private boolean existeCodigo(String codigoC)
                        throws VideoRentAccionException{
        ClienteAsociado cliente;
        String mensaje;

        //Validar que el codigo sea de un cliente existente
        cliente = contexto.buscarClienteAsociado(codigoC);
        if(cliente == null){
            try{
                escritorE.write("i & " + codigoC + " 0");
                escritorE.newLine();
            }catch(IOException e){
                throw new VideoRentAccionException("No se pudo escribir la modificacion de tarjeta: " + e.getMessage());
            }
            return false;
        }
        return true;
    }

}